/*
 * Copyright 2010 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.miolr.module.core.social.security;

import java.io.IOException;
import java.util.List;

import javax.inject.Inject;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.commons.lang3.StringUtils;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.social.connect.Connection;
import org.springframework.social.connect.ConnectionData;
import org.springframework.social.connect.ConnectionFactory;
import org.springframework.social.connect.ConnectionFactoryLocator;
import org.springframework.social.connect.UsersConnectionRepository;
import org.springframework.social.connect.support.OAuth1ConnectionFactory;
import org.springframework.social.connect.support.OAuth2ConnectionFactory;
import org.springframework.social.oauth1.AuthorizedRequestToken;
import org.springframework.social.oauth1.OAuth1Operations;
import org.springframework.social.oauth1.OAuth1Parameters;
import org.springframework.social.oauth1.OAuth1Version;
import org.springframework.social.oauth1.OAuthToken;
import org.springframework.social.oauth2.AccessGrant;
import org.springframework.social.oauth2.GrantType;
import org.springframework.social.oauth2.OAuth2Operations;
import org.springframework.social.oauth2.OAuth2Parameters;
import org.springframework.web.client.HttpClientErrorException;

public class OAuthAuthenticationFilter extends
		AbstractAuthenticationProcessingFilter {

	private String filterProcessesUrl = DEFAULT_FILTER_PROCESSES_URL;
	private static final String DEFAULT_FILTER_PROCESSES_URL = "/auth";
	private static final String SIGNUP_URL = "/signup";
	private String applicationUrl;
	private static final String OAUTH_TOKEN_ATTRIBUTE = "oauthToken";
	private static final String SOCIAL_CONNECTION_ATTRIBUTE = "social-connection";
	private String signupUrl = SIGNUP_URL;

	private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

	@Inject
	private ConnectionFactoryLocator factoryLocator;

	@Inject
	private UsersConnectionRepository connectionRepository;

	@Inject
	private UserDetailsService userDetailsService;

	/**
	 * @param signupUrl
	 *            the signupUrl to set
	 */
	public void setSignupUrl(String signupUrl) {
		this.signupUrl = signupUrl;
	}

	public OAuthAuthenticationFilter() {
		super(DEFAULT_FILTER_PROCESSES_URL);
	}

	/**
	 * @param defaultFilterProcessesUrl
	 */
	public OAuthAuthenticationFilter(String defaultFilterProcessesUrl) {
		super(defaultFilterProcessesUrl);
	}

	/**
	 * @param connectionRepository
	 *            the connectionRepository to set
	 */
	public void setConnectionRepository(
			UsersConnectionRepository connectionRepository) {
		this.connectionRepository = connectionRepository;
	}

	public String getFilterProcessesUrl() {
		return filterProcessesUrl;
	}

	public void setFilterProcessesUrl(String filterProcessesUrl) {
		this.filterProcessesUrl = filterProcessesUrl;
	}

	public void setFactoryLocator(ConnectionFactoryLocator factoryLocator) {
		this.factoryLocator = factoryLocator;
	}

	public void setApplicationUrl(String applicationUrl) {
		this.applicationUrl = applicationUrl;
	}

	public void setUserDetailsService(UserDetailsService userDetailsService) {
		this.userDetailsService = userDetailsService;
	}

	@Override
	public Authentication attemptAuthentication(HttpServletRequest request,
			HttpServletResponse response) throws AuthenticationException,
			IOException, ServletException {
		HttpSession session = request.getSession();
		String providerId = getRequestedProviderId(request);
		ConnectionFactory<?> connectionFactory = factoryLocator
				.getConnectionFactory(providerId);
		Connection<?> connection = null;
		String url;
		if (connectionFactory instanceof OAuth1ConnectionFactory<?>) {
			OAuth1ConnectionFactory<?> auth1ConnectionFactory = (OAuth1ConnectionFactory<?>) connectionFactory;
			String verifier = request.getParameter("oauth_verifier");
			if (StringUtils.isEmpty(verifier)) {
				OAuth1Operations oauthOperations = auth1ConnectionFactory
						.getOAuthOperations();
				OAuth1Parameters parameters = new OAuth1Parameters();
				if (oauthOperations.getVersion() == OAuth1Version.CORE_10) {
					parameters.setCallbackUrl(callbackUrl(request));
				}
				OAuthToken requestToken = fetchRequestToken(request,
						oauthOperations);
				session.setAttribute(OAUTH_TOKEN_ATTRIBUTE, requestToken);
				url = oauthOperations.buildAuthorizeUrl(
						requestToken.getValue(), parameters);
				response.sendRedirect(url);
				return null;
			} else {
				AuthorizedRequestToken requestToken = new AuthorizedRequestToken(
						extractCachedRequestToken(session), verifier);
				OAuthToken accessToken = auth1ConnectionFactory
						.getOAuthOperations().exchangeForAccessToken(
								requestToken, null);
				connection = auth1ConnectionFactory
						.createConnection(accessToken);
			}
		} else {
			String code = request.getParameter("code");
			OAuth2ConnectionFactory<?> auth2ConnectionFactory = (OAuth2ConnectionFactory<?>) connectionFactory;
			if (StringUtils.isEmpty(code)) {
				OAuth2Operations oauthOperations = auth2ConnectionFactory
						.getOAuthOperations();
				OAuth2Parameters parameters = new OAuth2Parameters();
				parameters.setRedirectUri(callbackUrl(request));
				String scope = request.getParameter("scope");
				if (scope != null) {
					parameters.setScope(scope);
				}
				url = oauthOperations.buildAuthorizeUrl(
						GrantType.AUTHORIZATION_CODE, parameters);
				response.sendRedirect(url);
				return null;
			} else {
				try {
					AccessGrant accessGrant = auth2ConnectionFactory
							.getOAuthOperations().exchangeForAccess(code,
									callbackUrl(request), null);
					connection = auth2ConnectionFactory
							.createConnection(accessGrant);
				} catch (HttpClientErrorException e) {
					logger.warn("HttpClientErrorException while completing connection: "
							+ e.getMessage());
					logger.warn("Response body: " + e.getResponseBodyAsString());
					throw e;
				}
			}
		}

		List<String> ids = connectionRepository
				.findUserIdsWithConnection(connection);
		ConnectionData data = connection.createData();
		if (ids.isEmpty()) {
			session.setAttribute(SOCIAL_CONNECTION_ATTRIBUTE, data);
			redirectStrategy.sendRedirect(request, response, this.signupUrl);
			return null;
		} else {
			connectionRepository.createConnectionRepository(ids.get(0)).updateConnection(connection);
			session.removeAttribute(SOCIAL_CONNECTION_ATTRIBUTE);
		}
		UserDetails details = userDetailsService.loadUserByUsername(ids.get(0));
		OAuthAuthenticationToken authenticationToken = new OAuthAuthenticationToken(
				details.getAuthorities(), details.getUsername(), data);
		return this.getAuthenticationManager()
				.authenticate(authenticationToken);
	}

	private String callbackUrl(HttpServletRequest request) {
		if (applicationUrl != null) {
			return applicationUrl + connectPath(request);
		} else {
			return request.getRequestURL().toString();
		}
	}

	private String connectPath(HttpServletRequest request) {
		String pathInfo = request.getPathInfo();
		return request.getServletPath() + (pathInfo != null ? pathInfo : "");
	}

	private OAuthToken extractCachedRequestToken(HttpSession session) {
		OAuthToken requestToken = (OAuthToken) session
				.getAttribute(OAUTH_TOKEN_ATTRIBUTE);
		session.removeAttribute(OAUTH_TOKEN_ATTRIBUTE);
		return requestToken;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.springframework.security.web.authentication.
	 * AbstractAuthenticationProcessingFilter
	 * #requiresAuthentication(javax.servlet.http.HttpServletRequest,
	 * javax.servlet.http.HttpServletResponse)
	 */
	@Override
	protected boolean requiresAuthentication(HttpServletRequest request,
			HttpServletResponse response) {
		String uri = request.getRequestURI();
		int pathParamIndex = uri.indexOf(';');

		if (pathParamIndex > 0) {
			// strip everything after the first semi-colon
			uri = uri.substring(0, pathParamIndex);
		}

		uri = uri.substring(0, uri.lastIndexOf("/"));

		if ("".equals(request.getContextPath())) {
			return uri.endsWith(filterProcessesUrl);
		}

		return uri.endsWith(request.getContextPath() + filterProcessesUrl);
	}

	protected String getRequestedProviderId(HttpServletRequest request) {
		String uri = request.getRequestURI();
		int pathParamIndex = uri.indexOf(';');

		if (pathParamIndex > 0) {
			// strip everything after the first semi-colon
			uri = uri.substring(0, pathParamIndex);
		}

		// uri must start with context path
		uri = uri.substring(request.getContextPath().length());

		// remaining uri must start with filterProcessesUrl
		if (!uri.startsWith(getFilterProcessesUrl())) {
			return null;
		}
		uri = uri.substring(getFilterProcessesUrl().length());

		// expect /filterprocessesurl/provider, not /filterprocessesurlproviderr
		if (uri.startsWith("/")) {
			return uri.substring(1);
		} else {
			return null;
		}
	}

	private OAuthToken fetchRequestToken(HttpServletRequest request,
			OAuth1Operations oauthOperations) {
		if (oauthOperations.getVersion() == OAuth1Version.CORE_10_REVISION_A) {
			return oauthOperations
					.fetchRequestToken(callbackUrl(request), null);
		}
		return oauthOperations.fetchRequestToken(null, null);
	}

}
